Coverage Report

Created: 2026-06-19 16:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\src\daemon\grid.rs
Line
Count
Source
1
//! Spatial grid model of the client windows.
2
//!
3
//! The tiler in [`super`] arranges `n` client windows on a grid of
4
//! `cols * rows` cells, with the final row possibly stretched to span
5
//! the full width when `n % cols != 0`. This module mirrors that layout
6
//! as a pure data structure so the enable/disable submenu can navigate
7
//! the cells spatially with arrow keys and `hjkl`.
8
9
#![deny(clippy::implicit_return)]
10
#![allow(clippy::needless_return, clippy::doc_overindented_list_items)]
11
12
use std::cmp::max;
13
use std::collections::HashMap;
14
15
use super::NavigationDirection;
16
use crate::utils::config::EdgeBehavior;
17
18
/// One client window's position on the spatial grid.
19
///
20
/// `col` is the leftmost upper-grid column the cell projects onto and
21
/// `col_span` is how many upper-grid columns it spans. Rows `0..rows-2`
22
/// are dense (`col_span == 1`); cells in a partial last row are stretched
23
/// (`col_span >= 1`).
24
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
25
pub(super) struct GridCell {
26
    /// Process id of the client owning this cell.
27
    pub pid: u32,
28
    /// 0-based row index.
29
    pub row: i32,
30
    /// Leftmost upper-grid column the cell projects onto.
31
    pub col: i32,
32
    /// Number of upper-grid columns the cell projects onto. Always
33
    /// `>= 1`; only `> 1` for partial-last-row cells.
34
    pub col_span: i32,
35
    /// 0-based position within the row, used for vertical roundtrips.
36
    pub pos_in_row: i32,
37
}
38
39
/// Spatial-grid view over the tracked client PIDs.
40
pub(super) struct ClientGrid {
41
    /// Number of columns in the dense upper rows.
42
    pub cols: i32,
43
    /// Total number of rows (`>= 1` whenever there is at least one cell).
44
    pub rows: i32,
45
    /// Cell count in the last row. `0` means the last row is also dense;
46
    /// otherwise `1..cols` last-row cells are stretched proportionally.
47
    last_row_count: i32,
48
    /// Cells sorted by `(row, col)` so the top-left cell is at index `0`.
49
    cells: Vec<GridCell>,
50
    /// PID lookup table.
51
    by_pid: HashMap<u32, usize>,
52
}
53
54
/// Compute the grid dimensions for `n` clients on a workspace with the
55
/// given aspect ratio.
56
///
57
/// Must match the formula used by the tiler in
58
/// [`super::determine_client_spatial_attributes`] so layout and
59
/// navigation stay in sync.
60
///
61
/// # Arguments
62
///
63
/// * `n`           - Number of client windows.
64
/// * `aspect`      - `workspace_width / workspace_height` (including
65
///                   frame padding) of the available area.
66
/// * `aspect_adj`  - The `aspect_ratio_adjustment` daemon config.
67
///
68
/// # Returns
69
///
70
/// `(cols, rows)`, each clamped to a minimum of `1`.
71
6
pub(super) fn grid_dimensions(n: i32, aspect: f64, aspect_adj: f64) -> (i32, i32) {
72
6
    let cols = max(((n as f64).sqrt() * (aspect + aspect_adj)) as i32, 1);
73
6
    let rows = max((n as f64 / cols as f64).ceil() as i32, 1);
74
6
    return (cols, rows);
75
6
}
76
77
impl ClientGrid {
78
    /// Build the grid from `(pid, tile_index)` pairs and the dimensions
79
    /// returned by [`grid_dimensions`] for the same `layout_n`.
80
    ///
81
    /// `tile_index` is the position each client was assigned the last
82
    /// time the tiler positioned its window. Surviving clients keep
83
    /// their `tile_index` across closures, so passing them here together
84
    /// with the layout's original `layout_n` produces a grid whose cells
85
    /// land at the same `(row, col)` the user sees on screen - with
86
    /// gaps where a window was closed but no retile has happened yet.
87
    ///
88
    /// # Arguments
89
    ///
90
    /// * `cells`     - `(pid, tile_index)` pairs for every surviving
91
    ///                 client.
92
    /// * `layout_n`  - The `number_of_consoles` the on-screen layout was
93
    ///                 last computed with. Used to derive the
94
    ///                 partial-last-row stretch.
95
    /// * `cols`      - Columns from [`grid_dimensions`] for `layout_n`.
96
    /// * `rows`      - Rows from [`grid_dimensions`] for `layout_n`.
97
    ///
98
    /// # Returns
99
    ///
100
    /// A populated [`ClientGrid`].
101
12
    pub(super) fn from_tiled_pids(
102
12
        cells: &[(u32, usize)],
103
12
        layout_n: i32,
104
12
        cols: i32,
105
12
        rows: i32,
106
12
    ) -> Self {
107
12
        let last_row_count = if cols > 0 { layout_n % cols } else { 
00
};
108
12
        let mut grid_cells = Vec::with_capacity(cells.len());
109
51
        for &(pid, tile_index) in 
cells12
{
110
51
            let idx = tile_index as i32;
111
51
            let row = if cols > 0 { idx / cols } else { 
00
};
112
51
            let pos_in_row = if cols > 0 { idx % cols } else { 
00
};
113
51
            let (col, col_span) = if row == rows - 1 && 
last_row_count != 025
{
114
3
                let left = (pos_in_row * cols) / last_row_count;
115
3
                let right = ((pos_in_row + 1) * cols - 1) / last_row_count;
116
3
                (left, right - left + 1)
117
            } else {
118
48
                (pos_in_row, 1)
119
            };
120
51
            grid_cells.push(GridCell {
121
51
                pid,
122
51
                row,
123
51
                col,
124
51
                col_span,
125
51
                pos_in_row,
126
51
            });
127
        }
128
80
        
grid_cells12
.
sort_by_key12
(|c| return (c.row, c.col));
129
12
        let by_pid = grid_cells
130
12
            .iter()
131
12
            .enumerate()
132
51
            .
map12
(|(i, c)| return (c.pid, i))
133
12
            .collect();
134
12
        return ClientGrid {
135
12
            cols,
136
12
            rows,
137
12
            last_row_count,
138
12
            cells: grid_cells,
139
12
            by_pid,
140
12
        };
141
12
    }
142
143
    /// Look up the cell owned by `pid`.
144
    ///
145
    /// # Arguments
146
    ///
147
    /// * `pid` - Process id to look up.
148
    ///
149
    /// # Returns
150
    ///
151
    /// `Some(&GridCell)` when present, `None` otherwise.
152
55
    pub(super) fn cell(&self, pid: u32) -> Option<&GridCell> {
153
55
        return self.by_pid.get(&pid).map(|&i| return 
&self.cells[i]53
);
154
55
    }
155
156
    /// PID of the top-left cell, or `None` for an empty grid. Used to
157
    /// re-anchor the submenu selection onto a sensible visual default.
158
2
    pub(super) fn top_left_pid(&self) -> Option<u32> {
159
2
        return self.cells.first().map(|c| return c.pid);
160
2
    }
161
162
    /// `true` when the grid has no cells.
163
33
    pub(super) fn is_empty(&self) -> bool {
164
33
        return self.cells.is_empty();
165
33
    }
166
167
    /// Compute the anchor column for a cell. Horizontal moves overwrite
168
    /// the in-flight anchor with the destination cell's anchor.
169
    ///
170
    /// Upper-row cells: their `col` (each cell occupies exactly one
171
    /// upper-grid column). Partial-last-row cells: the upper-grid column
172
    /// containing the cell's x-midpoint. The latter makes a Down + Up
173
    /// roundtrip return to the original cell from any starting point.
174
    ///
175
    /// # Arguments
176
    ///
177
    /// * `cell` - The destination cell.
178
    ///
179
    /// # Returns
180
    ///
181
    /// The anchor column for the cell.
182
5
    pub(super) fn anchor_for(&self, cell: &GridCell) -> i32 {
183
5
        if cell.row == self.rows - 1 && 
self.last_row_count != 01
{
184
0
            return ((2 * cell.pos_in_row + 1) * self.cols) / (2 * self.last_row_count);
185
5
        }
186
5
        return cell.col;
187
5
    }
188
189
    /// Compute the next selection after one navigation keystroke.
190
    ///
191
    /// # Arguments
192
    ///
193
    /// * `pid`        - Currently highlighted PID.
194
    /// * `anchor_col` - Anchor column carried from earlier moves.
195
    /// * `direction`  - Direction of the keystroke.
196
    /// * `edge`       - Behavior when the move would leave the grid.
197
    ///
198
    /// # Returns
199
    ///
200
    /// `Some((new_pid, new_anchor_col))` on a successful step.
201
    /// `None` when `pid` is not present in this grid (caller should
202
    /// re-anchor).
203
23
    pub(super) fn step(
204
23
        &self,
205
23
        pid: u32,
206
23
        anchor_col: i32,
207
23
        direction: NavigationDirection,
208
23
        edge: EdgeBehavior,
209
23
    ) -> Option<(u32, i32)> {
210
23
        let current = self.cell(pid)
?0
;
211
23
        return match direction {
212
            NavigationDirection::Left | NavigationDirection::Right => {
213
8
                self.step_horizontal(current, anchor_col, direction, edge)
214
            }
215
            NavigationDirection::Up | NavigationDirection::Down => {
216
15
                Some(self.step_vertical(current, anchor_col, direction, edge))
217
            }
218
        };
219
23
    }
220
221
    /// Horizontal step within `current.row`. Returns `None` only when
222
    /// the row somehow contains no cells (cannot happen for a valid
223
    /// `current` looked up from the grid). A clamped no-op preserves
224
    /// the in-flight `anchor_col` so a subsequent vertical step still
225
    /// targets the column the user originally carried over.
226
8
    fn step_horizontal(
227
8
        &self,
228
8
        current: &GridCell,
229
8
        anchor_col: i32,
230
8
        direction: NavigationDirection,
231
8
        edge: EdgeBehavior,
232
8
    ) -> Option<(u32, i32)> {
233
8
        let mut row_cells: Vec<&GridCell> = self
234
8
            .cells
235
8
            .iter()
236
46
            .
filter8
(|c| return c.row == current.row)
237
8
            .collect();
238
28
        
row_cells8
.
sort_by_key8
(|c| return c.col);
239
15
        let 
pos8
=
row_cells.iter()8
.
position8
(|c| return c.pid == current.pid)
?0
;
240
8
        let 
next3
= match direction {
241
            NavigationDirection::Left => {
242
2
                if pos == 0 {
243
2
                    match edge {
244
2
                        EdgeBehavior::Clamp => return Some((current.pid, anchor_col)),
245
0
                        EdgeBehavior::Wrap => *row_cells.last()?,
246
                    }
247
                } else {
248
0
                    row_cells[pos - 1]
249
                }
250
            }
251
            NavigationDirection::Right => {
252
6
                if pos + 1 >= row_cells.len() {
253
4
                    match edge {
254
3
                        EdgeBehavior::Clamp => return Some((current.pid, anchor_col)),
255
1
                        EdgeBehavior::Wrap => *row_cells.first()
?0
,
256
                    }
257
                } else {
258
2
                    row_cells[pos + 1]
259
                }
260
            }
261
0
            _ => return None,
262
        };
263
3
        return Some((next.pid, self.anchor_for(next)));
264
8
    }
265
266
    /// Vertical step into the target row, preserving the in-flight
267
    /// `anchor_col`.
268
15
    fn step_vertical(
269
15
        &self,
270
15
        current: &GridCell,
271
15
        anchor_col: i32,
272
15
        direction: NavigationDirection,
273
15
        edge: EdgeBehavior,
274
15
    ) -> (u32, i32) {
275
15
        let 
target_row13
= match direction {
276
            NavigationDirection::Up => {
277
6
                if current.row == 0 {
278
2
                    match edge {
279
1
                        EdgeBehavior::Clamp => return (current.pid, anchor_col),
280
1
                        EdgeBehavior::Wrap => self.rows - 1,
281
                    }
282
                } else {
283
4
                    current.row - 1
284
                }
285
            }
286
            NavigationDirection::Down => {
287
9
                if current.row + 1 >= self.rows {
288
1
                    match edge {
289
1
                        EdgeBehavior::Clamp => return (current.pid, anchor_col),
290
0
                        EdgeBehavior::Wrap => 0,
291
                    }
292
                } else {
293
8
                    current.row + 1
294
                }
295
            }
296
0
            _ => return (current.pid, anchor_col),
297
        };
298
13
        let row_cells: Vec<&GridCell> = self
299
13
            .cells
300
13
            .iter()
301
79
            .
filter13
(|c| return c.row == target_row)
302
13
            .collect();
303
13
        if row_cells.is_empty() {
304
0
            return (current.pid, anchor_col);
305
13
        }
306
13
        let is_partial_last_row = target_row == self.rows - 1 && 
self.last_row_count != 07
;
307
13
        let best = row_cells
308
13
            .into_iter()
309
38
            .
min_by_key13
(|c| {
310
38
                return (
311
38
                    self.anchor_distance(c, anchor_col, is_partial_last_row),
312
38
                    c.col,
313
38
                );
314
38
            })
315
13
            .expect("row_cells just checked non-empty");
316
13
        return (best.pid, anchor_col);
317
15
    }
318
319
    /// Spatial distance between a cell and an anchor column, used to
320
    /// pick the target on a vertical step. Dense rows reduce to
321
    /// `|c.col - anchor|`; partial-last-row cells use their stretched
322
    /// x-extent so the cell whose midpoint is closest to the anchor's
323
    /// centerline wins. The result is in arbitrary integer units valid
324
    /// only for comparisons within the same row.
325
38
    fn anchor_distance(&self, cell: &GridCell, anchor_col: i32, is_partial_last_row: bool) -> i64 {
326
38
        if is_partial_last_row {
327
12
            let cell_mid = (2 * cell.pos_in_row as i64 + 1) * self.cols as i64;
328
12
            let anchor_mid = (2 * anchor_col as i64 + 1) * self.last_row_count as i64;
329
12
            return (cell_mid - anchor_mid).abs();
330
26
        }
331
26
        return (2 * cell.col as i64 - 2 * anchor_col as i64).abs();
332
38
    }
333
}